<template>
    <div id="app">
        <div class="wrapper">
            <h1 class="title">图片转字符工具</h1>
            <div class="line"></div>
            <el-row type="flex">
                <el-col :span="6">
                    <el-input v-model="picWidth"><template slot="prepend">目标图片宽度：</template></el-input>
                </el-col>
                <el-col :span="6">
                    <el-input v-model="transformPlaceholder"><template slot="prepend">转换字符：</template></el-input>
                </el-col>
                <el-col :span="6">
                    <el-button @click="transform">开始转换</el-button>
                </el-col>
            </el-row>

            <div class="line"></div>
            <el-upload
                    action="https://jsonplaceholder.typicode.com/posts/"
                    :show-file-list="false"
                    :auto-upload="false"
                    list-type="picture-card"
                    :on-change="fileChange">
                <i class="el-icon-plus"></i>
                <div slot="tip" class="el-upload__tip">选择你要转换的图片</div>
            </el-upload>
            <el-dialog :visible.sync="dialogVisible" width="80%"
            title="图片转化结果">
                <FramesModal :frames="transformResult" :file="file" v-if="dialogVisible"/>
            </el-dialog>
            <img id="img" v-if="imageUrl" :style="{width: picWidth+'px'}" :src="imageUrl" alt="">
            <p class="showimg" id="showimg"><span></span></p>
            <p class="css_code" id="css_code"></p>
        </div>
    </div>
</template>

<script>
    import HelloWorld from './components/HelloWorld.vue'
    import FramesModal from './components/FramesModal.vue'

    const FONT_WIDTH = 6
    const FONT_HEIGHT = 12
    const DEFAULT_AVAILABLE_TEXTS = '@#&$%863!i1uazvno~;*^+-. '
    const IMAGE_TYPES_RE = {
        PNG: /image\/png/,
        JPG: /image\/jpe?g/,
        GIF: /image\/gif/,
    }
    const MAX_COLOR_PIXELS = 255

    export default {
        data() {
            return {
                picWidth: 800,
                charIndex: 0,
                file: '',
                transformResult: '',
                transformPlaceholder: '@#&$%863!i1uazvno~;*^+-. ',
                show:false,
                imageUrl:'',
                dialogVisible: false,
            }
        },
        methods: {
            fileChange(file){
                console.log(file);
                this.file = file.raw;
                this.fileToImage(this.file);
            },
            transform(){
                console.log('imgload');
                let img = document.getElementById("img");
                this.onTransform(img, this.file);
            },
            /**
             * 获取rgb颜色的灰度
             *
             * @param {number} r
             * @param {number} g
             * @param {number} b
             * @returns
             */
            rgbToGray(r, g, b) {
                return (299 * r + 587 * g + 114 * b + 500) / 1000
            },
            chooseFile(event) {
                this.show = false;
                let file = event.target.files[0];
                // 检查能否读取图片
                if (!file) {
                    return;
                }
                // 检查图片类型
                if (!(file.type.indexOf('image') == 0 && file.type && /\.(?:jpg|png|gif)$/.test(file.name))) {
                    alert('图片只能是jpg,gif,png');
                    return;
                }
                console.log('check ok');
                this.file = file;
                this.fileToImage(file);
            },
            fileToImage(file) {
                let reader = new FileReader();
                reader.readAsDataURL(file);//读取图像文件 result 为 DataURL, DataURL 可直接 赋值给 img.src
                reader.onload = (event) => {
                    this.imageUrl = event.target.result;//base64
                }
            },
            fileToCanvas(file, canvasId){
                var reader = new FileReader();
                reader.readAsDataURL(file);//读取图像文件 result 为 DataURL, DataURL 可直接 赋值给 img.src
                reader.onload = function(event){
                    var image = new Image();
                    image.src = event.target.result;
                    image.onload = function(){
                        var canvas = document.createElement('canvas');
                        canvas.width = image.width;
                        canvas.height = image.height
                        var imageCanvas = canvas.getContext("2d");
                        imageCanvas.drawImage(image, 0, 0);
                    }
                }
            },
            onTransform(image, file) {
                /**
                 * @type {HTMLImageElement}
                 */
                const framesData = this.getImageDatas(image, file)
                framesData.forEach(frameData => {
                    frameData.text = this.transformImageToText(frameData.data, this.createGrayToTextFunc(this.transformPlaceholder || DEFAULT_AVAILABLE_TEXTS))
                })
                console.log(framesData);
                this.show = true;
                this.transformResult = framesData;
                this.dialogVisible = true;
            },
            getImageDatas(image, file) {
                const type = this.getImageType(file.type)
                if (type === 'GIF') {
                    return this.getGif(image)
                } else {
                    return this.getNoGif(image)
                }
            },
            /**
             * 获取图片类型(png, jpg, gif)
             *
             * @
             * @param {string} type
             * @returns
             */
            getImageType(type) {
                for (const typeName in IMAGE_TYPES_RE) {
                    const typeRe = IMAGE_TYPES_RE[typeName]
                    if (typeRe.test(type)) {
                        return typeName
                    }
                }
            },
            /**
             * 将图像的一帧转成字符数组
             *
             * @param {ImageData} imageData
             * @param {Function} [grayToText=defaultGrayToText]
             * @returns
             */
            transformImageToText(imageData, grayToText = createGrayToTextFunc()) {
                const dataArr = imageData.data
                const width = imageData.width
                const height = imageData.height
                const lines = []
                for (let h = 0; h < height; h += FONT_HEIGHT) {
                    let line = ''
                    for (let w = 0; w < width; w += FONT_WIDTH) {
                        const i = (w + width * h) * 4
                        const gray = this.rgbToGray(dataArr[i], dataArr[i + 1], dataArr[i + 2])
                        line += grayToText(gray)
                    }
                    lines.push(line)
                }
                return lines
            },
            /**
             * 灰度转字符函数生成器 乱序
             *
             * @param {string | Array<string>} texts
             * @returns
             */
            createGrayToTextFunc(texts = DEFAULT_AVAILABLE_TEXTS) {
                const grayGap = MAX_COLOR_PIXELS / texts.length

                /**
                 * 灰度转字符函数
                 *
                 * @param {number} gray
                 * @returns
                 */
                return (gray)=> {
                    let textIndex = (gray / grayGap) >> 0
                    if (textIndex >= texts.length) {
                        textIndex = texts.length - 1
                    }
                    return texts[textIndex]
                }
            },
            /**
             * 灰度转字符函数生成器 顺序
             *
             * @param {string | Array<string>} texts
             * @returns
             */
            createGrayToTextFuncOrderly(texts = DEFAULT_AVAILABLE_TEXTS) {
                const grayGap = MAX_COLOR_PIXELS / texts.length
                let index = 0;
                /**
                 * 灰度转字符函数
                 *
                 * @param {number} gray
                 * @returns
                 */
                return (gray)=> {
                    let textIndex = (gray / grayGap) >> 0
                    if (textIndex >= texts.length) {
                        return ' ';
                    }
                    this.charIndex ++;
                    if (this.charIndex >= texts.length) {
                        this.charIndex = 0;
                    }
                    return texts[this.charIndex]
                }
            },
            /**
             * 获取gif图像信息
             *
             * @param {HTMLImageElement} image
             * @returns
             */
            getGif(image) {
                const data = this.decode(image.src)
                const {width: rawWidth, height: rawHeight, frames} = this.gifParser(data)
                const framesData = []
                for (let i = 0; i < frames.length; i++) {
                    const frameData = new ImageData(frames[i].data, rawWidth, rawHeight)
                    framesData.push({
                        data: this.scaleFrameData(frameData, image),
                        delay: frames[i].delay
                    })
                }
                return framesData
            },
            scaleFrameData(frameData, image) {
                // 方法来源：
                // https://stackoverflow.com/questions/3448347/how-to-scale-an-imagedata-in-html-canvas

                // 存放一帧原图像到canvas中
                const nartualCanvas = document.createElement('canvas')
                const nartualCtx = nartualCanvas.getContext('2d')
                nartualCanvas.width = image.naturalWidth
                nartualCanvas.height = image.naturalHeight
                nartualCtx.putImageData(frameData, 0, 0)

                // 新建一个canvas，宽高设为目标宽高，并进行画布缩放
                const scaleCanvas = document.createElement('canvas')
                const scaleCtx = scaleCanvas.getContext('2d')
                scaleCanvas.width = image.width
                scaleCanvas.height = image.height
                const ratio = image.width / image.naturalWidth
                scaleCtx.scale(ratio, ratio)
                // 将保存的原图像使用drawImage绘制到新画布上
                scaleCtx.drawImage(nartualCanvas, 0, 0)

                return scaleCtx.getImageData(0, 0, image.width, image.height)
            },
            decode(uri) {
                // 找到分割 metadata 和 data 的逗号位置
                const commaIndex = uri.indexOf(',')

                const meta = uri.substring(5, commaIndex).split(';')  // 头部信息 metadata
                const type = meta[0] || 'text/plain'

                const data = uri.substring(commaIndex + 1)  // 内容信息
                let byteString
                if (meta.indexOf('base64') >= 0) {
                    byteString = atob(data)
                } else {
                    byteString = unescape(data)
                }
                const buffer = new Uint8Array(byteString.length)
                for (let i = 0; i < byteString.length; i++) {
                    buffer[i] = byteString.charCodeAt(i)
                }
                buffer.type = type

                return buffer
            },
            gifParser (buffer) {
                const view = new Uint8Array(buffer)
                let offset = 0
                const info = {
                    /**
                     * 署名和版本号信息
                     */
                    header: '',
                    /**
                     * 图像宽度
                     */
                    w: 0,
                    /**
                     * 图像高度
                     */
                    h: 0,
                    /**
                     * m - 全局颜色列表标志
                     */
                    m: 0,
                    /**
                     * cr - 颜色深度(Color ResoluTion)
                     */
                    cr: 0,
                    /**
                     * s - 分类标志(Sort Flag)
                     */
                    s: 0,
                    /**
                     * pixel - 全局颜色列表大小
                     */
                    pixel: 0,
                    /**
                     * 背景颜色: 背景颜色在全局颜色列表中的索引
                     */
                    bgIndex: 0,
                    /**
                     * 像素宽高比
                     */
                    radio: 1,
                    /**
                     * 全局颜色列表信息
                     */
                    colorTable: [],
                    frames: [],
                    comment: ''
                }
                let frame

                /**
                 * 读取指定长度字节
                 *
                 * @param {number} len
                 * @returns
                 */
                function readByte(len) {
                    return view.slice(offset, (offset += len))
                }

                /**
                 * 读取gif头部信息 (起始 6 byte)
                 * 包括署名 (Signature) 和版本号 (Version)
                 */
                function getHeader() {
                    const GIF_SIGNATURE_RE = /^GIF8[79]a$/
                    const GIF_SIGNATURE_RE_LOWER = /^gif8[79]a$/
                    info.header = ''
                    readByte(6).forEach(byte => {
                        info.header += String.fromCharCode(byte)
                    })
                    if (!info.header.match(GIF_SIGNATURE_RE) && !info.header.match(GIF_SIGNATURE_RE_LOWER)) {
                        throw new Error('GIF署名错误')
                    }
                }

                /**
                 * 读取逻辑屏幕标识符 (Logical Screen Descriptor), 总共 7 byte
                 */
                function getScreenDesc() {
                    const arr = readByte(7)

                    info.w = arr[0] + (arr[1] << 8)
                    info.h = arr[2] + (arr[3] << 8)
                    info.m = 1 & (arr[4] >> 7)
                    info.cr = 7 & (arr[4] >> 4)
                    info.s = 1 & (arr[4] >> 3)
                    info.pixel = 7 & arr[4]
                    info.bgIndex = arr[5]
                    info.radio = arr[6]

                    // 若全局颜色列表标志位置数了，则读取全局颜色列表
                    if (info.m) {
                        // 2 << pixel === 1 << (pixel + 1) === 2^(pixel+1)
                        info.colorTable = readByte((2 << info.pixel) * 3)
                    }
                }

                function decode() {
                    let bytes = readByte(1)

                    switch (bytes[0]) {
                        case 0x21: //扩展块, 33
                            extension()
                            break
                        case 0x2c: //图像标识符, 44
                            bytes = readByte(9)
                            frame.img = {
                                x: bytes[0] + (bytes[1] << 8),
                                y: bytes[2] + (bytes[3] << 8),
                                w: bytes[4] + (bytes[5] << 8),
                                h: bytes[6] + (bytes[7] << 8),
                                m: 1 & (bytes[8] >> 7),
                                /**
                                 * i - 交织标志(Interlace Flag)
                                 */
                                i: 1 & (bytes[8] >> 6),
                                s: 1 & (bytes[8] >> 5),
                                r: 3 & (bytes[8] >> 3),
                                pixel: 7 & bytes[8],
                                colorTable: []
                            }

                            // 读取局部颜色列表信息
                            if (frame.img.m) {
                                frame.img.colorTable = readByte((2 << frame.img.pixel) * 3)
                            }

                            // 读取LZW编码的长度
                            frame.img.encodeSize = readByte(1)[0]

                            const encodeBuf = []
                            while (1) {
                                bytes = readByte(1)
                                if (bytes[0]) {
                                    // arr[0] 为当前块的大小（不包括自己这个字节）
                                    // 读取这个数据块的所有数据，并保存为一个数组
                                    readByte(bytes[0]).forEach(e => {
                                        encodeBuf.push(e)
                                    })
                                } else {
                                    frame.img.encodeBuf = encodeBuf
                                    decode()
                                    break
                                }
                            }
                            break
                        case 0x3b: // 终结符，59
                            console.log('Parse Finish.')
                            break
                        default:
                            // 未知标识符，报错
                            throw new Error('Unknow Byte Flag:' + bytes[0])
                    }
                }

                function extension() {
                    let bytes = readByte(1)
                    switch (bytes[0]) {
                        case 0xff: // 应用程序扩展, 255
                            if (readByte(1)[0] == 11) {
                                info.appVersion = ''
                                readByte(11).forEach(function(e) {
                                    info.appVersion += String.fromCharCode(e)
                                })
                                while(1) {
                                    bytes = readByte(1)
                                    if (bytes[0]) {
                                        readByte(bytes[0])
                                    } else {
                                        decode()
                                        break
                                    }
                                }
                            } else {
                                throw new Error('解析出错')
                            }
                            break
                        case 0xf9: // 图形控制扩展, 249
                            // 块大小固定为 4
                            if (readByte(1)[0] === 4) {
                                bytes = readByte(4)
                                frame = {}
                                frame.extension = {
                                    // 处置方法
                                    disp: 7 & (bytes[0] >> 2),
                                    // 用户输入标准
                                    i: 1 & (bytes[0] >> 1),
                                    // 透明色标志
                                    t: 1 & bytes[0],
                                    // 延迟时间
                                    delay: (bytes[1] + (bytes[2] << 8)) * 10,
                                    // 透明色索引
                                    tranIndex: bytes[3]
                                }
                                info.frames.push(frame)
                                // 标识块终结符
                                if (readByte(1)[0] == 0) {
                                    decode()
                                } else {
                                    throw new Error('解析出错')
                                }
                            } else {
                                throw new Error('解析出错')
                            }
                            break
                        case 0xfe: // 注释块, 254
                            bytes = readByte(1)
                            if (bytes[0]) {
                                readByte(bytes[0]).forEach(function(e) {
                                    info.comment += String.fromCharCode(e)
                                })
                                if (readByte(1)[0] == 0) {
                                    decode()
                                }
                            }
                            break
                        default:
                            console.log(bytes)
                            break
                    }
                }

                function calcPixel() {
                    let lastImageData
                    info.frames.forEach(function(frame) {
                        const canvas = document.createElement('canvas')
                        const ctx = canvas.getContext('2d')
                        // 判断使用局部颜色列表还是全局颜色列表
                        const colorTable = frame.img.m ? frame.img.colorTable : info.colorTable

                        canvas.width = info.w
                        canvas.height = info.h
                        let imageData = ctx.getImageData(0, 0, info.w, info.h)
                        function lzw(arr, min) {
                            const clearCode = 1 << min
                            const eofCode = clearCode + 1
                            let size = min + 1
                            let dict = []
                            let pos = 0

                            /**
                             * 清除已存储的压缩映射表数据
                             */
                            function clear() {
                                dict = []
                                size = min + 1
                                for (let i = 0; i < clearCode; i++) {
                                    dict[i] = [i]
                                }
                                dict[clearCode] = []
                                dict[eofCode] = null
                            }

                            /**
                             * 读取指定位数 bit，并转成10进制数字
                             *
                             * @param {number} size
                             * @returns
                             */
                            function readBit(size) {
                                let code = 0
                                for (let i = 0; i < size; i++) {
                                    // arr[pos / 8] & (1 << (pos % 8))
                                    if (arr[pos >> 3] & (1 << (pos & 7))) {
                                        code |= 1 << i
                                    }
                                    pos++
                                }
                                return code
                            }

                            function decode() {
                                const out = []
                                let last // 前缀
                                let code // 后缀
                                while (1) {
                                    last = code
                                    code = readBit(size)

                                    if (code == clearCode) {
                                        // 第一个读到的 code 必然是 clearCode，会初始化数据
                                        clear()
                                        continue
                                    } else if (code == eofCode) {
                                        break
                                    } else if (code < dict.length) {
                                        // code 映射已知
                                        if (last !== clearCode) {
                                            dict.push(dict[last].concat(dict[code][0]))
                                        }
                                    } else if (code === dict.length) {
                                        // code 映射未知
                                        dict.push(dict[last].concat(dict[last][0]))
                                    } else {
                                        throw new Error('LZW解析出错')
                                    }
                                    out.push.apply(out, dict[code])
                                    if (dict.length === 1 << size && size < 12) {
                                        // 取值位数+1
                                        size++
                                    }
                                }
                                return out
                            }

                            return {
                                decode: decode
                            }
                        }
                        lzw(frame.img.encodeBuf, frame.img.encodeSize)
                            .decode()
                            .forEach(function(pixel, i) {
                                imageData.data[i * 4] = colorTable[pixel * 3]
                                imageData.data[i * 4 + 1] = colorTable[pixel * 3 + 1]
                                imageData.data[i * 4 + 2] = colorTable[pixel * 3 + 2]
                                imageData.data[i * 4 + 3] = 255
                                if (frame.extension.t && pixel == frame.extension.tranIndex) {
                                    imageData.data[i * 4 + 3] = 0
                                }
                            })
                        ctx.putImageData(imageData, frame.img.x, frame.img.y, 0, 0, frame.img.w, frame.img.h)
                        imageData = ctx.getImageData(0, 0, info.w, info.h)

                        if (lastImageData) {
                            for (let i = 0; i < imageData.data.length; i += 4) {
                                // 某个像素不透明度为 0，用上一帧的相应位置的数据填补
                                if (imageData.data[i + 3] == 0) {
                                    imageData.data[i] = lastImageData.data[i]
                                    imageData.data[i + 1] = lastImageData.data[i + 1]
                                    imageData.data[i + 2] = lastImageData.data[i + 2]
                                    imageData.data[i + 3] = lastImageData.data[i + 3]
                                }
                            }
                        }
                        if (frame.extension.disp === 1 || frame.extension.disp === 0) {
                            lastImageData = imageData
                        }
                        frame.data = imageData.data
                    })
                }

                getHeader()
                getScreenDesc()
                decode()
                calcPixel()

                return {
                    width: info.w,
                    height: info.h,
                    frames: info.frames.map(frame => ({
                        /**
                         * @type {Uint8ClampedArray}
                         */
                        data: frame.data,
                        /**
                         * @type {number}
                         */
                        delay: frame.extension.delay
                    }))
                }
            },
            /**
             * 获取非gif图像信息
             *
             * @param {HTMLImageElement} image
             * @returns
             */
            getNoGif(image) {
                console.log("image", image);
                console.log("image.width", image.width);
                const canvas = document.createElement('canvas')
                const ctx = canvas.getContext('2d')
                console.log('image.naturalWidth',image.naturalWidth);
                const ratio = image.width / image.naturalWidth
                // 将画布内容缩放到与当前图像一致
                canvas.width = image.width
                canvas.height = image.height
                ctx.scale(ratio, ratio)
                // 绘制当前图像到canvas上
                ctx.drawImage(image, 0, 0)
                const imageData = ctx.getImageData(0, 0, image.width, image.height)
                return [{data: imageData, delay: null}]
            }
        },
        created(){
            this.show = false;
        },
        name: 'app',
        components:
            {
                FramesModal
            }
    }
</script>

<style>
    #app {
        font-family: 'Avenir', Helvetica, Arial, sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        text-align: center;
        color: #2c3e50;
        padding-top: 60px;
        background-color: beige;
        width: 100%;
        height: 1000px;
    }
    .line{
        width: 100%;
        height: 0px;
        border: 1px solid;
    }
    .wrapper{
        margin: 0 100px;
    }
</style>